﻿#region NeoPatterns
// 
// NeoPatterns framework is released under LGPLv3 license.
// See http://www.opensource.org/licenses/lgpl-3.0.html for more details.
// 
// Please visit http://neopatterns.wikidot.com for more information about NeoPatterns
// 
// Copyright (c) Pascal Craponne 2008
// 
#endregion

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using NeoPatterns.Utility;

namespace NeoPatterns.Core.Aop.Implementation
{
    internal class InheritanceAspectBuilder : IAspectBuilder
    {
        /// <summary>
        /// Represents a type build to be advised
        /// </summary>
        protected class AdvisedType
        {
            public Type Type;
            public FieldInfo AspectsMember;
        }

        /// <summary>
        /// An interface related to an advisor with claims (for private space in each instance)
        /// </summary>
        protected class ClaimInterface
        {
            public Type InterfaceType;
            /// <summary>
            /// The property allows private space to advisors (implementing IClaimingAdvisor)
            /// </summary>
            public PropertyInfo ClaimPropertyInfo;
        }

        private readonly IDictionary<Type, AdvisedType> advisedTypes = new Dictionary<Type, AdvisedType>();
        private readonly IDictionary<Type, ClaimInterface> claimInterfaces = new Dictionary<Type, ClaimInterface>();

        private readonly AppDomain appDomain;

        private InheritanceAspectBuilder(AppDomain appDomain)
        {
            this.appDomain = appDomain;
        }

        public InheritanceAspectBuilder()
            : this(AppDomain.CurrentDomain)
        {

        }

        /// <summary>
        /// Returns the base type for object to intercept.
        /// If provided parameter is an interface, returns System.Object, otherwise returns the type
        /// </summary>
        /// <param name="baseTypeOrInterface"></param>
        /// <returns></returns>
        protected virtual Type GetBaseType(Type baseTypeOrInterface)
        {
            if (baseTypeOrInterface.IsInterface)
                return typeof(object);
            return baseTypeOrInterface;
        }

        /// <summary>
        /// Returns a list of interfaces to implement (one or none, depending on the parameter type)
        /// </summary>
        /// <param name="baseTypeOrInterface"></param>
        /// <returns></returns>
        protected virtual Type[] GetImplementedInterfaces(Type baseTypeOrInterface)
        {
            if (baseTypeOrInterface.IsInterface)
                return new[] { baseTypeOrInterface };
            return new Type[0];
        }

        /// <summary>
        /// Returns list of methods to override
        /// </summary>
        /// <param name="baseTypeOrInterface"></param>
        /// <returns></returns>
        protected virtual IEnumerable<MethodInfo> GetOverridableMethods(Type baseTypeOrInterface)
        {
            var bindingFlags = BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.Instance;
            foreach (var method in baseTypeOrInterface.GetMethods(bindingFlags))
            {
                // static methods can not be overriden
                if (method.IsStatic)
                    continue;
                // non-virtual methods can not be overriden
                if (!method.IsVirtual && !method.IsAbstract)
                    continue;
                // and of course, sealed methods can not either be overriden
                if (method.IsFinal)
                    continue;
                // when doing changes to IL code, uncomment the following two lines, to avoid problems
                if (method.DeclaringType == typeof(object))
                    continue;
                yield return method;
            }
        }

        private ModuleBuilder moduleBuilder;
        private AssemblyBuilder assemblyBuilder; // this is a bit dirty
        protected virtual ModuleBuilder ModuleBuilder
        {
            get
            {
                if (moduleBuilder != null)
                    return moduleBuilder;
                var assemblyName = string.Format(CultureInfo.InvariantCulture, "npda{0:8X}", new Random().Next(100000000));
                assemblyBuilder = appDomain.DefineDynamicAssembly(new AssemblyName(assemblyName), AssemblyBuilderAccess.RunAndSave);
                var interceptorAssemblyFileName = string.Format(CultureInfo.InvariantCulture, "{0}.dll", assemblyName);
                moduleBuilder = assemblyBuilder.DefineDynamicModule(interceptorAssemblyFileName, interceptorAssemblyFileName, true);
                return moduleBuilder;
            }
        }

        protected virtual AdvisedType GetAdvisedType(Type baseTypeOrInterface, IEnumerable<Aspect> aspects)
        {
            AdvisedType advisedType;

            if (advisedTypes.TryGetValue(baseTypeOrInterface, out advisedType))
                return advisedType;

            return BuildAdvisedType(baseTypeOrInterface, aspects);
        }

        protected virtual AdvisedType BuildAdvisedType(Type baseTypeOrInterface, IEnumerable<Aspect> aspects)
        {
            var advisedType = new AdvisedType();

            // computed names
            // semantics are:
            // - <TypeName>' (prime) the interceptor inherits (derives from) the class
            // - <method>(round arrow) since it calls back the original method
            var interceptorTypeName = string.Format(CultureInfo.InvariantCulture, "{1}.{0}\u2032",
                                                    baseTypeOrInterface.Name, baseTypeOrInterface.Namespace);

            var typeAttributes = TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed;
            var implementedInterfaces = new List<Type>(GetImplementedInterfaces(baseTypeOrInterface));
            var claimInterfaceAdvisors = new List<Type>();
            foreach (var aspect in aspects)
            {
                var advisor2 = aspect.Advisor as IClaimingAdvisor;
                if (advisor2 != null)
                    claimInterfaceAdvisors.Add(GetClaimInterface(advisor2).InterfaceType);
            }
            implementedInterfaces.AddRange(claimInterfaceAdvisors);
            var typeBuilder = ModuleBuilder.DefineType(interceptorTypeName, typeAttributes,
                                                       GetBaseType(baseTypeOrInterface),
                                                       implementedInterfaces.ToArray());

            var interceptorFieldAttributes = FieldAttributes.Private | FieldAttributes.NotSerialized;
            const string aspectsFieldName = "aspects\u2606";
            var aspectsFieldBuilder = typeBuilder.DefineField(aspectsFieldName, typeof(IEnumerable<Aspect>),
                                                              interceptorFieldAttributes);

            // for each method, we set the original call (if available)
            // and the interceptor
            // claim interfaces are not processed here
            var overriddenMethods = new Dictionary<MethodInfo, MethodInfo>();
            foreach (var method in GetOverridableMethods(baseTypeOrInterface))
            {
                // original call
                MethodBuilder originalCallMethodBuilder = null;
                if (!method.IsAbstract)
                    originalCallMethodBuilder = BuildBaseCallMethod(typeBuilder, method);

                var methodBuilder = BuildInterceptorMethod(typeBuilder, method, aspectsFieldBuilder, originalCallMethodBuilder);
                // keep original method <-> interceptor method
                overriddenMethods[method] = methodBuilder;
            }

            // foreach claim interface, we implement the property and backing field
            foreach (var claimInterface in claimInterfaceAdvisors)
            {
                BuildClaimInterfaceImplementation(typeBuilder, claimInterface);
            }

            advisedType.Type = typeBuilder.CreateType();
            advisedType.AspectsMember = advisedType.Type.GetFieldInfo(aspectsFieldBuilder);
            foreach (var overridenMethod in overriddenMethods.Keys)
            {
                var interceptorMethod = advisedType.Type.GetMethodInfo(overriddenMethods[overridenMethod]);
                AspectInvocation.OriginalMethods[interceptorMethod] = overridenMethod;
            }

            // dirty: to be removed
            //assemblyBuilder.Save(ModuleBuilder.ScopeName);

            advisedTypes[baseTypeOrInterface] = advisedType;
            return advisedType;
        }

        protected virtual void BuildClaimInterfaceImplementation(TypeBuilder typeBuilder, Type claimInterface)
        {
            foreach (var member in claimInterface.GetMembers())
            {
                var propertyInfo = member as PropertyInfo;
                if (propertyInfo != null)
                {
                    var backingFieldName = string.Format("_{0}BackingField\u2606", claimInterface.Name);
                    var backingFieldBuilder = typeBuilder.DefineField(backingFieldName, propertyInfo.PropertyType, FieldAttributes.Private);

                    var propertyBuilder = typeBuilder.DefineProperty(claimInterface.Name, PropertyAttributes.HasDefault, propertyInfo.PropertyType, null);

                    var methodAttributes = MethodAttributes.Private | MethodAttributes.HideBySig |
                                           MethodAttributes.NewSlot | MethodAttributes.Virtual |
                                           MethodAttributes.Final;

                    var propertyGetMethod = propertyInfo.GetGetMethod();
                    if (propertyGetMethod != null)
                    {
                        var getMethodName = string.Format("get_{0}", claimInterface.Name);
                        var propertyGetMethodBuilder = typeBuilder.DefineMethod(getMethodName, methodAttributes, propertyInfo.PropertyType, null);
                        typeBuilder.DefineMethodOverride(propertyGetMethodBuilder, propertyGetMethod);
                        propertyBuilder.SetGetMethod(propertyGetMethodBuilder);

                        var getMethodIL = propertyGetMethodBuilder.GetILGenerator();
                        getMethodIL.Emit(OpCodes.Ldarg_0);
                        getMethodIL.Emit(OpCodes.Ldfld, backingFieldBuilder);
                        getMethodIL.Emit(OpCodes.Ret);
                    }

                    var propertySetMethod = propertyInfo.GetSetMethod();
                    if (propertySetMethod != null)
                    {
                        var setMethodName = string.Format("set_{0}", claimInterface.Name);
                        var propertySetMethodBuilder = typeBuilder.DefineMethod(setMethodName, methodAttributes, typeof(void), new[] { propertyInfo.PropertyType });
                        typeBuilder.DefineMethodOverride(propertySetMethodBuilder, propertySetMethod);
                        propertyBuilder.SetSetMethod(propertySetMethodBuilder);

                        var setMethodIL = propertySetMethodBuilder.GetILGenerator();
                        setMethodIL.Emit(OpCodes.Ldarg_0);
                        setMethodIL.Emit(OpCodes.Ldarg_1);
                        setMethodIL.Emit(OpCodes.Stfld, backingFieldBuilder);
                        setMethodIL.Emit(OpCodes.Ret);
                    }
                }
            }
        }

        /// <summary>
        /// Returns a claim interface, related to an advisor.
        /// The claim interface allows to store custom data in the target instance (implementing the claim interface)
        /// </summary>
        /// <param name="claimingAdvisor"></param>
        /// <returns></returns>
        protected virtual ClaimInterface GetClaimInterface(IClaimingAdvisor claimingAdvisor)
        {
            lock (claimInterfaces)
            {
                ClaimInterface claimInterface;
                var advisorType = claimingAdvisor.GetType();
                if (claimInterfaces.TryGetValue(advisorType, out claimInterface))
                    return claimInterface;

                return BuildClaimInterface(advisorType);
            }
        }

        protected virtual ClaimInterface BuildClaimInterface(Type advisorType)
        {
            var claimInterface = new ClaimInterface();

            // create interface type
            var interfaceName = string.Format("I{0}Claim\u2606", advisorType.Name);
            var typeAttributes = TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Interface;
            var interfaceBuilder = ModuleBuilder.DefineType(interfaceName, typeAttributes);

            // then add a property,
            var propertyType = typeof(object);
            var propertyBuilder = interfaceBuilder.DefineProperty("Claim", PropertyAttributes.HasDefault,
                                                                  propertyType, null);

            // ... with get and set
            var methodAttributes = MethodAttributes.SpecialName | MethodAttributes.Public | MethodAttributes.Virtual |
                                   MethodAttributes.Abstract | MethodAttributes.HideBySig | MethodAttributes.NewSlot;
            var getMethodBuilder = interfaceBuilder.DefineMethod("get_Claim", methodAttributes, propertyType, null);
            var setMethodBuilder = interfaceBuilder.DefineMethod("set_Claim", methodAttributes, typeof(void),
                                                                 new[] { propertyType });

            propertyBuilder.SetGetMethod(getMethodBuilder);
            propertyBuilder.SetSetMethod(setMethodBuilder);

            claimInterface.InterfaceType = interfaceBuilder.CreateType();
            claimInterface.ClaimPropertyInfo = claimInterface.InterfaceType.GetPropertyInfo(propertyBuilder);
            claimInterfaces[advisorType] = claimInterface;
            return claimInterface;
        }

        /// <summary>
        /// Builds the interceptor method
        /// </summary>
        /// <param name="typeBuilder"></param>
        /// <param name="method"></param>
        /// <param name="crosscutsFieldBuilder"></param>
        /// <param name="originalCallMethodBuilder"></param>
        protected virtual MethodBuilder BuildInterceptorMethod(TypeBuilder typeBuilder, MethodInfo method, FieldBuilder crosscutsFieldBuilder, MethodBuilder originalCallMethodBuilder)
        {
            var methodParameters = method.GetParameters();
            // interception method does
            // 1. create an invocation object
            // 2. call its Proceed() method

            // public static void Invocation.Proceed(object target, object[] parameters, 
            //                                       IEnumerable<Aspect> crosscuts,
            //                                       MethodInfo targetMethod, Action<IInvocation> baseMethod)

            var parameterTypes = (from p in method.GetParameters() select p.ParameterType).ToArray();

            var methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig
                                   | MethodAttributes.Virtual | MethodAttributes.Final;

            var methodBuilder = typeBuilder.DefineMethod(method.Name, methodAttributes,
                                                         CallingConventions.HasThis, method.ReturnType,
                                                         parameterTypes.ToArray());

            var typeNames = (from t in method.GetGenericArguments() select t.Name).ToArray();
            if (typeNames.Length > 0)
                methodBuilder.DefineGenericParameters(typeNames);

            var methodIL = methodBuilder.GetILGenerator();

            // 1.1 target
            methodIL.Emit(OpCodes.Ldarg_0);

            // 1.2 parameters
            // creates parameters array
            var parametersLocal = methodIL.DeclareLocal(typeof(object[]));
            parametersLocal.SetLocalSymInfo("parameters");

            methodIL.EmitLdc(methodParameters.Length);
            methodIL.Emit(OpCodes.Newarr, typeof(object));
            methodIL.EmitStloc(parametersLocal);
            // pushes parameters
            for (int parameterIndex = 0; parameterIndex < methodParameters.Length; parameterIndex++)
            {
                var parameter = methodParameters[parameterIndex];
                // we don't care about output parameters
                if (!parameter.IsOut)
                {
                    methodIL.EmitLdloc(parametersLocal); // array
                    methodIL.EmitLdc(parameterIndex); // array index
                    methodIL.EmitLdarg(parameterIndex + 1); // loads given parameter...
                    if (parameter.IsRef()) // ...if ref, loads it as referenced value
                        methodIL.EmitLdind(parameter.GetParameterType());
                    methodIL.EmitBoxIfNecessary(parameter.GetParameterType()); // ... and boxes it
                    methodIL.Emit(OpCodes.Stelem_Ref);
                }
            }
            methodIL.EmitLdloc(parametersLocal);

            // 1.3 crosscuts
            methodIL.Emit(OpCodes.Ldarg_0);
            methodIL.Emit(OpCodes.Ldfld, crosscutsFieldBuilder);

            // 1.4 method info
            methodIL.Emit(OpCodes.Call, ReflectionUtility.GetMethodInfo(() => MethodBase.GetCurrentMethod()));
            methodIL.Emit(OpCodes.Castclass, typeof(MethodInfo));

            // 1.5 base method
            if (originalCallMethodBuilder != null)
            {
                methodIL.Emit(OpCodes.Ldnull);
                methodIL.Emit(OpCodes.Ldftn, originalCallMethodBuilder);
                methodIL.Emit(OpCodes.Newobj,
                              typeof(Action<IInvocation>).GetConstructor(new[] { typeof(object), typeof(IntPtr) }));
            }
            else
            {
                methodIL.Emit(OpCodes.Ldnull);
            }

            // call Interceptor.Process()
            methodIL.Emit(OpCodes.Call, ReflectionUtility.GetMethodInfo(() => AspectInvocation.Process(null, null, null, null, null)));

            // get return value
            if (method.ReturnType != typeof(void))
                methodIL.EmitUnboxOrCastIfNecessary(method.ReturnType);
            else
                methodIL.Emit(OpCodes.Pop); // if no return type, ignore Process() result

            // loads back out/ref parameters
            for (int parameterIndex = 0; parameterIndex < methodParameters.Length; parameterIndex++)
            {
                var parameter = methodParameters[parameterIndex];
                if (parameter.IsOut || parameter.IsRef())
                {
                    methodIL.EmitLdarg(parameterIndex + 1); // loads given parameter (it is a ref)
                    methodIL.EmitLdloc(parametersLocal); // array
                    methodIL.EmitLdc(parameterIndex); // array index
                    methodIL.Emit(OpCodes.Ldelem_Ref); // now we have boxed out/ref value
                    methodIL.EmitUnboxOrCastIfNecessary(parameter.GetParameterType());
                    methodIL.EmitStind(parameter.GetParameterType()); // result is stored in ref parameter
                }
            }

            // and return
            methodIL.Emit(OpCodes.Ret);

            return methodBuilder;
        }

        protected virtual MethodBuilder BuildBaseCallMethod(TypeBuilder typeBuilder, MethodInfo method)
        {
            var methodParameters = method.GetParameters();
            var originalCallMethodName = string.Format(CultureInfo.InvariantCulture, "{0}\u21BA", method.Name);

            var methodAttributes = MethodAttributes.Private | MethodAttributes.Static;

            var originalCallMethodBuilder = typeBuilder.DefineMethod(originalCallMethodName, methodAttributes,
                                                                     0, null,
                                                                     new[] { typeof(IInvocation) });

            var methodIL = originalCallMethodBuilder.GetILGenerator();

            var parametersArrayLocal = methodIL.DeclareLocal(typeof(object[]));
            parametersArrayLocal.SetLocalSymInfo("parameters");
            methodIL.Emit(OpCodes.Ldarg_0);
            methodIL.Emit(OpCodes.Callvirt, ReflectionUtility.GetPropertyInfo<IInvocation>(i => i.Parameters).GetGetMethod());
            methodIL.EmitStloc(parametersArrayLocal);
            //methodIL.Emit(OpCodes.Pop);

            // load Target (it is the "this")
            methodIL.Emit(OpCodes.Ldarg_0);
            methodIL.Emit(OpCodes.Callvirt, ReflectionUtility.GetPropertyInfo<IInvocation>(i => i.Target).GetGetMethod());

            // transform Arguments[] to real arguments
            var locals = new LocalBuilder[methodParameters.Length];

            // each argument is loaded, eventually unboxed and pushed as argument
            for (int parameterIndex = 0; parameterIndex < methodParameters.Length; parameterIndex++)
            {
                var parameter = methodParameters[parameterIndex];
                // if we're in a ref or in parameter, load its initial value (otherwise, we don't care)
                if (!parameter.IsOut)
                {
                    methodIL.EmitLdloc(parametersArrayLocal);
                    methodIL.EmitLdc(parameterIndex);
                    methodIL.Emit(OpCodes.Ldelem_Ref);
                    methodIL.EmitUnboxOrCastIfNecessary(parameter.GetParameterType());
                    // with objects by reference, we store their value on a local variable
                    // we will give a pointer to this variable to the called method
                    if (parameter.IsRef())
                    {
                        locals[parameterIndex] = methodIL.DeclareLocal(parameter.GetParameterType());
                        methodIL.EmitStloc(locals[parameterIndex]);
                        methodIL.Emit(OpCodes.Ldloca, locals[parameterIndex]);
                    }
                }
                else
                {
                    // for output variables, we create a local variable, with the correct type
                    // a provide their address
                    locals[parameterIndex] = methodIL.DeclareLocal(parameter.GetParameterType());
                    methodIL.Emit(OpCodes.Ldloca, locals[parameterIndex]);
                }
            }

            // call original method
            methodIL.Emit(OpCodes.Call, method);

            // get the return value
            if (method.ReturnType != typeof(void))
            {
                // we have the value on stack, box it and store to IInvocation.ReturnValue
                // we could avoid the use of a local variable by pushing the arg0 before calling original method+args
                // but the current implementation is more readable and easier to maintain
                // ... place in local variable
                var returnValueLocal = methodIL.DeclareLocal(method.ReturnType);
                returnValueLocal.SetLocalSymInfo("returnValue");
                methodIL.EmitStloc(returnValueLocal);
                // ... call the setter
                methodIL.Emit(OpCodes.Ldarg_0);
                methodIL.EmitLdloc(returnValueLocal);
                methodIL.EmitBoxIfNecessary(method.ReturnType);
                methodIL.Emit(OpCodes.Callvirt, ReflectionUtility.GetPropertyInfo<IInvocation>(i => i.ReturnValue).GetSetMethod());
            }

            // now, load return values
            for (int parameterIndex = 0; parameterIndex < methodParameters.Length; parameterIndex++)
            {
                var local = locals[parameterIndex];
                if (local != null) // ... this is a shortcut: if we have a variable, we have an out/ref value
                {
                    methodIL.EmitLdloc(parametersArrayLocal); // array
                    methodIL.EmitLdc(parameterIndex); // index
                    methodIL.EmitLdloc(local); // value...
                    methodIL.EmitBoxIfNecessary(local.LocalType); // ...[boxed]
                    methodIL.Emit(OpCodes.Stelem_Ref);
                }
            }

            methodIL.Emit(OpCodes.Ret);

            return originalCallMethodBuilder;
        }

        public virtual Type GetAdvisedType(Type baseTypeOrInterface, IEnumerable<Aspect> aspects, out MemberInfo aspectsMember)
        {
            var advisedType = GetAdvisedType(baseTypeOrInterface, aspects);
            aspectsMember = advisedType.AspectsMember;
            return advisedType.Type;
        }

        /// <summary>
        /// Builds object with interception and aspects
        /// </summary>
        /// <param name="baseTypeOrInterface"></param>
        /// <param name="aspects"></param>
        /// <returns></returns>
        public virtual object CreateInstance(Type baseTypeOrInterface, IEnumerable<Aspect> aspects)
        {
            MemberInfo aspectsMember;
            var type = GetAdvisedType(baseTypeOrInterface, aspects, out aspectsMember);
            object instance = Activator.CreateInstance(type);
            aspectsMember.SetMemberValue(instance, aspects);
            foreach (var aspect in aspects)
            {
                var advisor2 = aspect.Advisor as IClaimingAdvisor;
                if (advisor2 != null)
                {
                    advisor2.Claims = GetClaimInterface(advisor2).ClaimPropertyInfo;
                    advisor2.InitializeClaims(instance);
                }
            }
            return instance;
        }
    }
}